﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.Diagnostics;

namespace BitcoinLibrary
{
    /**
    * Methods to serialize and de-serialize messages to the bitcoin network format as defined in the bitcoin protocol
    * specification at https://en.bitcoin.it/wiki/Protocol_specification
    *
    * To be able to serialize and deserialize new Message subclasses the following criteria needs to be met.
    * <ul>
    * <li>The proper Class instance needs to be mapped to it's message name in the names variable below</li>
    * <li>There needs to be a constructor matching: NetworkParameters params, byte[] payload</li>
    * <li>Message.bitcoinSerializeToStream() needs to be properly subclassed</li>
    * </ul>
    *
    */
    public class BitcoinSerializer
    {
        //private static final Logger log = LoggerFactory.getLogger(BitcoinSerializer.class);
        private const int COMMAND_LEN = 12;

        private NetworkParameters param;
        private bool usesChecksumming;

        private static Dictionary<Type, string> names = null;
        private static Dictionary<String, Message> messageConstructors = new Dictionary<String, Message>();

        public BitcoinSerializer()
        {
            if (names == null)
            {
                names = new Dictionary<Type, string>();
                names.Add(typeof(VersionMessage), "version");
                names.Add(typeof(InventoryMessage), "inv");
                names.Add(typeof(Block), "block");
                names.Add(typeof(GetDataMessage), "getdata");
                names.Add(typeof(Transaction), "tx");
                names.Add(typeof(AddressMessage), "addr");
                names.Add(typeof(Ping), "ping");
                names.Add(typeof(VersionAck), "verack");
                names.Add(typeof(GetBlocksMessage), "getblocks");
            }
        }

        /**
        * Constructs a BitcoinSerializer with the given behavior.
        *
        * @param params networkParams used to create Messages instances and termining packetMagic
        * @param usesChecksumming set to true if checkums should be included and expected in headers
        */
        public BitcoinSerializer(NetworkParameters param, bool usesChecksumming)
            : base()
        {
            this.param = param;
            this.usesChecksumming = usesChecksumming;

            // some Message subclasses can only be sent for now, ignore missing constructors
            //TODO: for (Class<? extends Message> c : names.keySet()) {
            //Constructor<? extends Message> ct = makeConstructor(c);
            //if (ct != null) {
            //messageConstructors.put(names.get(c),ct);
            //}
            //}
        }

        public void useChecksumming(bool usesChecksumming)
        {
            this.usesChecksumming = usesChecksumming;
        }


        /**
        * Writes message to to the output stream.
        */
        public void serialize(Message message, Stream outStream)
        {
            string name = names[message.GetType()];
            if (name == null)
            {
                throw new Exception("BitcoinSerializer doesn't currently know how to serialize " + message.GetType().Name);
            }

            byte[] header = new byte[4 + COMMAND_LEN + 4 + (usesChecksumming ? 4 : 0)];

            Utils.uint32ToByteArrayBE(param.packetMagic, header, 0);

            // The header array is initialized to zero by Java so we don't have to worry about
            // NULL terminating the string here.
            for (int i = 0; i < name.Length && i < COMMAND_LEN; i++)
            {
                //header[4 + i] = (byte) (name.codePointAt(i) & 0xFF);
                header[4 + i] = (byte)(name[i] & 0xFF);
            }

            byte[] payload = message.bitcoinSerialize();

            Utils.uint32ToByteArrayLE(payload.Length, header, 4 + COMMAND_LEN);

            if (usesChecksumming)
            {
                byte[] hash = Utils.doubleDigest(payload);
                Array.Copy(hash, 0, header, 4 + COMMAND_LEN + 4, 4);
            }

            outStream.Write(header, 0, header.Length);
            outStream.Write(payload, 0, payload.Length);

            Debug.WriteLine("Sending {} message: {}", name, Utils.bytesToHexString(header) + Utils.bytesToHexString(payload));
        }

        /**
        * Reads a message from the given InputStream and returns it.
        */
        public Message deserialize(Stream inStream)
        {
            // A BitCoin protocol message has the following format.
            //
            // - 4 byte magic number: 0xfabfb5da for the testnet or
            // 0xf9beb4d9 for production
            // - 12 byte command in ASCII
            // - 4 byte payload size
            // - 4 byte checksum
            // - Payload data
            //
            // The checksum is the first 4 bytes of a SHA256 hash of the message payload. It isn't
            // present for all messages, notably, the first one on a connection.
            //
            // Satoshi's implementation ignores garbage before the magic header bytes. We have to do the same because
            // sometimes it sends us stuff that isn't part of any message.
            seekPastMagicBytes(inStream);
            // Now read in the header.
            byte[] header = new byte[COMMAND_LEN + 4 + (usesChecksumming ? 4 : 0)];
            int readCursor = 0;
            while (readCursor < header.Length)
            {
                int bytesRead = inStream.Read(header, readCursor, header.Length - readCursor);
                if (bytesRead == -1)
                {
                    // There's no more data to read.
                    throw new Exception("Socket is disconnected");
                }
                readCursor += bytesRead;
            }

            int cursor = 0;

            // The command is a NULL terminated string, unless the command fills all twelve bytes
            // in which case the termination is implicit.
            String command;
            int mark = cursor;
            for (; header[cursor] != 0 && cursor - mark < COMMAND_LEN; cursor++) ;
            byte[] commandBytes = new byte[cursor - mark];
            Array.Copy(header, mark, commandBytes, 0, cursor - mark);
            try
            {
                command = System.Text.Encoding.ASCII.GetString(commandBytes);//new String(commandBytes, "US-ASCII");
            }
            catch (Exception e)
            {
                throw e; // Cannot happen.
            }
            cursor = mark + COMMAND_LEN;

            int size = (int)Utils.readUint32(header, cursor);
            cursor += 4;

            if (size > Message.MAX_SIZE)
                throw new ProtocolException("Message size too large: " + size);

            // Old clients don't send the checksum.
            byte[] checksum = new byte[4];
            if (usesChecksumming)
            {
                // Note that the size read above includes the checksum bytes.
                Array.Copy(header, cursor, checksum, 0, 4);
                cursor += 4;
            }

            // Now try to read the whole message.
            readCursor = 0;
            byte[] payloadBytes = new byte[size];
            while (readCursor < payloadBytes.Length - 1)
            {
                int bytesRead = inStream.Read(payloadBytes, readCursor, size - readCursor);
                if (bytesRead == -1)
                {
                    throw new Exception("Socket is disconnected");
                }
                readCursor += bytesRead;
            }

            // Verify the checksum.
            if (usesChecksumming)
            {
                byte[] hash = Utils.doubleDigest(payloadBytes);
                if (checksum[0] != hash[0] || checksum[1] != hash[1] ||
                checksum[2] != hash[2] || checksum[3] != hash[3])
                {
                    throw new ProtocolException("Checksum failed to verify, actual " +
                    Utils.bytesToHexString(hash) +
                    " vs " + Utils.bytesToHexString(checksum));
                }
            }

            //if (log.isDebugEnabled()) 
            //{
            //Debug.WriteLine("Received {} byte '{}' message: {}", new Object[]{
            //size,
            //command,
            //Utils.bytesToHexString(payloadBytes)
            //});
            //}

            //try 
            //{
            // TODO: Constructor<? extends Message> c = messageConstructors.get(command);
            //if (c == null) 
            //{
            //throw new ProtocolException("No support for deserializing message with name " + command);
            //}
            //return c.newInstance(param, payloadBytes);
            //} 
            //catch (Exception e) 
            //{
            //throw new ProtocolException("Error deserializing message " + Utils.bytesToHexString(payloadBytes) + "\n", e);
            //}
            //}

            //private Constructor<? extends Message> makeConstructor(Class<? extends Message> c) 
            //{
            //Class<?> parTypes[] = new Class<?>[2];
            //parTypes[0] = NetworkParameters.class;
            //parTypes[1] = byte[].class;

            //try {
            //return c.getDeclaredConstructor(parTypes);
            //} catch (NoSuchMethodException e) {
            return null;
            //}

        }

        private void seekPastMagicBytes(Stream inStream)
        {
            int magicCursor = 3; // Which byte of the magic we're looking for currently.
            while (true)
            {
                int b = inStream.ReadByte(); // Read a byte.
                if (b == -1)
                {
                    // There's no more data to read.
                    throw new Exception("Socket is disconnected");
                }
                // We're looking for a run of bytes that is the same as the packet magic but we want to ignore partial
                // magics that aren't complete. So we keep track of where we're up to with magicCursor.
                int expectedByte = 0xFF & (int)(param.packetMagic >> (magicCursor * 8));    // TODO: >>>
                if (b == expectedByte)
                {
                    magicCursor--;
                    if (magicCursor < 0)
                    {
                        // We found the magic sequence.
                        return;
                    }
                    else
                    {
                        // We still have further to go to find the next message.
                    }
                }
                else
                {
                    magicCursor = 3;
                }
            }
        }
    }

}
